package com.szeldon.declatest.declaration.reader;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import nu.xom.ParsingException;
import nu.xom.ValidityException;

import com.szeldon.declatest.declaration.TestsSuiteDeclaration;
import com.szeldon.declatest.declaration.TestsSuiteElementsEnum;
import com.szeldon.declatest.declaration.endpoints.EndpointDeclaration;
import com.szeldon.declatest.declaration.endpoints.EndpointsElementsEnum;
import com.szeldon.declatest.declaration.props.PropertyFileDeclaration;
import com.szeldon.declatest.declaration.props.PropertyFilesElementsEnum;
import com.szeldon.declatest.declaration.reader.exceptions.BadSettingsException;
import com.szeldon.declatest.declaration.reader.exceptions.NotSupportedSettingsException;
import com.szeldon.declatest.declaration.reader.exceptions.TestSuiteFileException;
import com.szeldon.declatest.declaration.reader.settings.ISettingsReaderPlugin;
import com.szeldon.declatest.declaration.scenario.ScenarioDeclaration;
import com.szeldon.declatest.declaration.scenario.ScenariosTypesEnum;
import com.szeldon.declatest.declaration.scenario.parts.CheckpointPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.DoScenarioStepPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ElsePartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.EndPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ErrorPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.IfPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.NotComputedPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ReturnPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ScenarioPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ScenarioPartsTypeEnum;
import com.szeldon.declatest.declaration.settings.SettingsDeclaration;
import com.szeldon.declatest.declaration.settings.WebServiceSettingsPartDeclaration;
import com.szeldon.declatest.declaration.steps.ScenarioStepDeclaration;
import com.szeldon.declatest.declaration.steps.ScenarioStepsElementsEnum;
import com.szeldon.utils.ValuesUtils;

/**
 * Reader class for reading from XML file with declarations of tests.
 * 
 * @author szeldon
 * 
 */
public class XMLDeclarationReader extends TestsDeclarationReader {

    private static Logger logger = Logger.getLogger(XMLDeclarationReader.class.getName());

    /**
     * Creates reader from XML files with declaration of test suite.
     * 
     * @param filename
     *            Name of XML file to read declaration from.
     */
    public XMLDeclarationReader(String filename) {
        super(filename);
    }

    @Override
    public TestsSuiteDeclaration abstractReadTestsSuiteDeclaration() throws FileNotFoundException, IOException,
            TestSuiteFileException {
        TestsSuiteDeclaration suiteDeclaration = new TestsSuiteDeclaration();

        String fileContent = readFromFile();

        // TODO validate against schema
        logger.warning("TODO: validate against schema!");

        Builder builder = new Builder();
        try {
            Document document = builder.build(fileContent, null);

            parseTestSuite(document, suiteDeclaration);

        } catch (ValidityException e) {
            throw new TestSuiteFileException("Exception concerning validity during parsing content of " + filename
                    + " XML with declaration of test suite.", e);
        } catch (ParsingException e) {
            throw new TestSuiteFileException("Exception during parsing XML with declaration of test suite.", e);
        } catch (IllegalArgumentException e) {
            throw new TestSuiteFileException("Bad file. " + e.getMessage(), e);
        } catch (UnsupportedOperationException e) {
            throw new TestSuiteFileException("Bad file. " + e.getMessage(), e);
        } catch (NotSupportedSettingsException e) {
            throw new TestSuiteFileException("No plugin found for settings. " + e.getMessage());
        } catch (BadSettingsException e) {
            throw new TestSuiteFileException("Bad settings. " + e.getMessage());
        }

        return suiteDeclaration;
    }

    /**
     * Fills {@link TestsSuiteDeclaration} object with data from XML document.
     * 
     * @param document
     * @param suiteDeclaration
     * @throws TestSuiteFileException
     *             Thrown when XML isn't proper.
     * @throws NotSupportedSettingsException
     * @throws BadSettingsException
     */
    private void parseTestSuite(Document document, TestsSuiteDeclaration suiteDeclaration)
            throws TestSuiteFileException, NotSupportedSettingsException, BadSettingsException {
        // parses and sets attributes of the whole test suite
        Element rootElement = document.getRootElement();
        for (int i = 0; i < rootElement.getAttributeCount(); i++) {
            Attribute attribute = rootElement.getAttribute(i);
            suiteDeclaration.addTestSuiteAttribute(attribute.getLocalName(), attribute.getValue());
        }

        Elements rootChildrenElements = rootElement.getChildElements();
        int rootChildrenCount = rootChildrenElements.size();

        if (rootChildrenCount > TestsSuiteElementsEnum.values().length) {
            throw new TestSuiteFileException("Too many tags (" + rootChildrenCount + ") inside TestSuite tag.");
        }

        // goes through children and analyzes them
        for (int i = 0; i < rootChildrenCount; i++) {
            Element currentElement = rootChildrenElements.get(i);
            String currentElementName = currentElement.getLocalName();
            if (currentElementName.equals(TestsSuiteElementsEnum.PROPERTY_FILES.toString())) {
                parsePropertyFiles(currentElement, suiteDeclaration);
            } else if (currentElementName.equals(TestsSuiteElementsEnum.SETTINGS.toString())) {
                parseSettings(currentElement, suiteDeclaration);
            } else if (currentElementName.equals(TestsSuiteElementsEnum.ENDPOINTS.toString())) {
                parseEndpoints(currentElement, suiteDeclaration);
            } else if (currentElementName.equals(TestsSuiteElementsEnum.SCENARIO_STEPS.toString())) {
                parseScenarioSteps(currentElement, suiteDeclaration);
            } else if (currentElementName.equals(TestsSuiteElementsEnum.SCENARIOS.toString())) {
                parseScenarios(currentElement, suiteDeclaration);
            } else {
                throw new TestSuiteFileException("TestSuite XML can't contain '" + currentElement.getLocalName()
                        + "' as a child of the root.");
            }
        }

    }

    private void parsePropertyFiles(Element element, TestsSuiteDeclaration suiteDeclaration)
            throws TestSuiteFileException {
        Elements propertyFileElements = element.getChildElements();

        int propertyFilesCount = propertyFileElements.size();
        for (int i = 0; i < propertyFilesCount; i++) {
            Element currentFileElement = propertyFileElements.get(i);
            String elementName = currentFileElement.getLocalName();

            if (!elementName.equals(PropertyFilesElementsEnum.PROPERTY_FILE.getName())) {
                throw new TestSuiteFileException("'" + PropertyFilesElementsEnum.PROPERTY_FILE.getName()
                        + "' expected but there's '" + elementName + "'.");
            }
            try {
                PropertyFileDeclaration propertyFile = new PropertyFileDeclaration(currentFileElement.getAttribute(0)
                        .getValue());
                suiteDeclaration.addPropertyFile(propertyFile);
            } catch (IndexOutOfBoundsException e) {
                throw new TestSuiteFileException("There aren't enough attributes for PropertyFile.");
            }

        }
    }

    private void parseSettings(Element element, TestsSuiteDeclaration suiteDeclaration) throws TestSuiteFileException,
            NotSupportedSettingsException, BadSettingsException {
        Elements propertyFileElements = element.getChildElements();

        int propertyFilesCount = propertyFileElements.size();
        for (int i = 0; i < propertyFilesCount; i++) {
            Element currentSettingsElement = propertyFileElements.get(i);
            String elementName = currentSettingsElement.getLocalName();

            // uses pluging to create service-specific settings
            ISettingsReaderPlugin settingsPlugin = settingsPluginsMap.get(elementName);
            if (settingsPlugin == null) {
                throw new NotSupportedSettingsException("Can't handle settings for \"" + elementName
                        + "\" service. No reader plugin for settings set.");
            }
            Map<String, String> settingsMap = createSettingsMap(currentSettingsElement);
            SettingsDeclaration settingsPart = settingsPlugin.createSettingsDeclaration(
                    currentSettingsElement.getAttributeValue("id"),
                    currentSettingsElement.getAttributeValue("description"), settingsMap);

            if (settingsPart == null) {
                throw new IllegalStateException("Settings plugin didn't return a settings declaration for type \""
                        + elementName + "\".");
            }

            suiteDeclaration.addSettings(settingsPart);

        }

    }

    /**
     * Creates a map of attribute's names and values.
     * 
     * @param settingsElement
     * @return
     */
    private Map<String, String> createSettingsMap(Element settingsElement) {

        Map<String, String> settingsMap = new HashMap<String, String>();

        for (int i = 0; i < settingsElement.getAttributeCount(); i++) {
            Attribute currentAttribute = settingsElement.getAttribute(i);
            settingsMap.put(currentAttribute.getLocalName(), currentAttribute.getValue());
        }

        return settingsMap;
    }

    private void parseEndpoints(Element element, TestsSuiteDeclaration suiteDeclaration) throws TestSuiteFileException {
        Elements endpointElements = element.getChildElements();

        int endpointsCount = endpointElements.size();
        for (int i = 0; i < endpointsCount; i++) {
            Element currentEndpointElement = endpointElements.get(i);
            String elementName = currentEndpointElement.getLocalName();

            if (!elementName.equals(EndpointsElementsEnum.ENDPOINT.getName())) {
                throw new TestSuiteFileException("Endpoints can't contain '" + elementName + "'.");
            }

            EndpointDeclaration endpointPart = new EndpointDeclaration(currentEndpointElement.getAttributeValue("id"),
                    currentEndpointElement.getAttributeValue("description"),
                    currentEndpointElement.getAttributeValue("url"),
                    currentEndpointElement.getAttributeValue("settingsId"));

            suiteDeclaration.addEndpoint(endpointPart);
        }
    }

    private void parseScenarioSteps(Element element, TestsSuiteDeclaration suiteDeclaration)
            throws TestSuiteFileException {
        Elements endpointElements = element.getChildElements();

        int endpointsCount = endpointElements.size();
        for (int i = 0; i < endpointsCount; i++) {
            Element currentStepElement = endpointElements.get(i);
            String elementName = currentStepElement.getLocalName();

            if (!elementName.equals(ScenarioStepsElementsEnum.SCENARIO_STEP.getName())) {
                throw new TestSuiteFileException("ScenarioSteps can't contain '" + elementName + "'.");
            }

            String id = ValuesUtils.checkNamedValueForNull("id", currentStepElement.getAttributeValue("id"));
            String description = ValuesUtils.checkNamedValueForNull("description",
                    currentStepElement.getAttributeValue("description"));
            String endpointId = ValuesUtils.checkNamedValueForNull("endpointId",
                    currentStepElement.getAttributeValue("endpointId"));
            String request = ValuesUtils.checkNamedValueForNull("request",
                    currentStepElement.getAttributeValue("request"));

            suiteDeclaration.addScenarioStep(new ScenarioStepDeclaration(id, description, endpointId, request));
        }

    }

    private void parseScenarios(Element element, TestsSuiteDeclaration suiteDeclaration) throws TestSuiteFileException {

        Elements parts = element.getChildElements();
        int partsCount = parts.size();
        for (int i = 0; i < partsCount; i++) {
            Element currentElement = parts.get(i);

            String currentName = currentElement.getLocalName();

            if (!currentName.equals(ScenariosTypesEnum.SCENARIO.getName())) {
                throw new TestSuiteFileException("Scenarios element can't contain '" + currentName + "' as its child.");
            }

            String id = currentElement.getAttributeValue("id");
            String description = currentElement.getAttributeValue("description");

            ScenarioDeclaration scenario = new ScenarioDeclaration(id, description);

            parseScenario(scenario, null, currentElement);

            suiteDeclaration.addScenarioStep(scenario);

        }

    }

    /**
     * Parses given element in search of scenario parts. Parts are added to given scenario or to scnarioPart. Second
     * option is used when scenarioPart isn't null. Otherwise, parts are added to scenario.
     * 
     * @param scenario
     * @param scenarioPart
     *            If null, subparts are added to scenario. If not null, subparts are added to it.
     * @param element
     */
    private void parseScenario(ScenarioDeclaration scenario, ScenarioPartDeclaration scenarioPart,
            Element scenarioElement) {

        // iterates over children of scenario element
        Elements childElements = scenarioElement.getChildElements();
        int childElementsCount = childElements.size();
        for (int i = 0; i < childElementsCount; i++) {

            Element currentChildElement = childElements.get(i);
            ScenarioPartDeclaration currentChildPart = mapElementToPart(currentChildElement);

            if (scenarioPart == null) {
                scenario.addScenarioPart(currentChildPart);
            } else {
                scenarioPart.addSubpart(currentChildPart);
            }

            // if there are children for current child
            if (currentChildElement.getChildCount() > 0) {
                parseScenario(scenario, currentChildPart, currentChildElement);
            }

        }

    }

    /**
     * Returns {@link ScenarioPartDeclaration} object created basing on provided element. Won't return null.
     * 
     * @param element
     * @return
     * @throws IllegalArgumentException
     *             Thrown when given element isn't supported.
     */
    public static ScenarioPartDeclaration mapElementToPart(Element element) throws IllegalArgumentException {

        ScenarioPartDeclaration part = null;

        String elementName = element.getLocalName();

        if (elementName.equals(ScenarioPartsTypeEnum.CHECKPOINT_PART.getName())) {
            part = new CheckpointPartDeclaration(element.getAttributeValue("id"),
                    element.getAttributeValue("description"));
        } else if (elementName.equals(ScenarioPartsTypeEnum.ELSE_PART.getName())) {
            part = new ElsePartDeclaration(element.getAttributeValue("left"), element.getAttributeValue("operator"),
                    element.getAttributeValue("right"), element.getAttributeValue("description"));
        } else if (elementName.equals(ScenarioPartsTypeEnum.END_PART.getName())) {
            part = new EndPartDeclaration(element.getAttributeValue("description"));
        } else if (elementName.equals(ScenarioPartsTypeEnum.ERROR_PART.getName())) {
            part = new ErrorPartDeclaration(element.getAttributeValue("description"));
        } else if (elementName.equals(ScenarioPartsTypeEnum.IF_PART.getName())) {
            part = new IfPartDeclaration(element.getAttributeValue("left"), element.getAttributeValue("operator"),
                    element.getAttributeValue("right"), element.getAttributeValue("description"));
        } else if (elementName.equals(ScenarioPartsTypeEnum.RETURN.getName())) {
            part = new ReturnPartDeclaration(element.getAttributeValue("checkpointId"));
        } else if (elementName.equals(ScenarioPartsTypeEnum.DO_SCENARIO_STEP_PART.getName())) {
            part = new DoScenarioStepPartDeclaration(element.getAttributeValue("stepId"));
        } else if (elementName.equals(ScenarioPartsTypeEnum.NOT_COMPUTED.getName())) {
            part = new NotComputedPartDeclaration(element.getAttributeValue("description"));
        } else {
            throw new IllegalArgumentException("Can't create part of scenario for '" + elementName + "'.");
        }

        return part;

    }

    private SettingsDeclaration createJmsSettings(Element currentSettingsElement) {
        throw new UnsupportedOperationException("JMS settings not yet supported.");
    }

    private SettingsDeclaration createDatabaseSettings(Element currentSettingsElement) {
        throw new UnsupportedOperationException("Database settings not yet supported.");
    }

    private SettingsDeclaration createWssSettings(Element settingsElement) {
        String settingsId = ValuesUtils.checkNamedValueForNull("settingsId", settingsElement.getAttributeValue("id"));
        String settingsDescription = ValuesUtils.checkNamedValueForNull("settingsDescription",
                settingsElement.getAttributeValue("description"));

        WebServiceSettingsPartDeclaration wssSettings = new WebServiceSettingsPartDeclaration(settingsId,
                settingsDescription);

        return wssSettings;
    }

}
